---
title: Addon migration guide for Storybook 10.0
sidebar:
  order: 9
  title: Migrate addons to 10.0
---

We sincerely appreciate the dedication and effort addon creators put into keeping the Storybook ecosystem vibrant and up-to-date. As Storybook evolves to version 10.0, bringing new features and improvements, this guide is here to assist you in migrating your addons from 9.x to 10.0. If you need to migrate your addon from an earlier version of Storybook, please first refer to the [Addon migration guide for Storybook 9.0](../../../release-9-1/docs/addons/addon-migration-guide.mdx).

<Callout variant="info">
  We also have a general [Storybook migration guide](../releases/migration-guide.mdx) that covers updates to your Storybook instance rather than your addon code.
</Callout>

## Dependency updates

You will need to update your Storybook dependencies. Peer dependencies must point to `^10.0.0` to ensure broad compatibility for your end users. Development dependencies can be set to `^10.0.0`, or to `next` if you want to try the latest prerelease all year round.

```jsonc title="package.json"
{
  "devDependencies": {
    "@storybook/addon-docs": "next",
    "@storybook/react-vite": "next",
    "storybook": "next"
  },
  "peerDependencies": {
    "storybook": "^10.0.0"
  }
}
```

If you still have `@storybook/addon-essentials`, `@storybook/addon-interactions`, `@storybook/addon-links`, or `@storybook/blocks` in your dependencies, you will need to remove them. These packages are empty since Storybook 9 and will no longer be published going forward.

### Supporting earlier versions

If your addon supports multiple major versions of Storybook, you can specify a wider version range in your peer dependencies:

```jsonc title="package.json"
{
  "name": "your-storybook-addon",
  "peerDependencies": {
    "storybook": "^9.0.0 || ^10.0.0"
  },
  "devDependencies": {
    "storybook": ">=10.0.0-0 <11.0.0-0" // For local development
  }
}
```

However, we recommend releasing a new major version of your addon alongside new major versions of Storybook. This practice:
1. Makes it easier to maintain your code
2. Allows you to take advantage of new features and improvements
3. Provides a clearer upgrade path for your users

## Key changes for addons

Here are the changes in version 10.0 that impact addon development.

### ESM-only builds

Storybook 10 requires all addons to be built as ESM-only. This change simplifies the build process and reduces maintenance overhead. You'll need to make many changes to `tsup.config.ts`, so it can be easier to copy the reference file in the [`addon-kit` repository](https://github.com/storybookjs/addon-kit/blob/main/tsup.config.ts).

This update brings the following changes:
* The Node target moves from Node 20.0 to Node 20.19
* You no longer need to build CJS files
* You no longer need to pass `globalManagerPackages` and `globalPreviewPackages`
* The `bundler` config in `package.json` no longer needs to be manually typed
* We recommend you stop using `exportEntries` and switch exported entries to `previewEntries` and `managerEntries` instead based on where they are consumed by your users

```diff title="tsup.config.ts"
- import { readFile } from "node:fs/promises";

import { defineConfig, type Options } from "tsup";

- import { globalPackages as globalManagerPackages } from "storybook/internal/manager/globals";
- import { globalPackages as globalPreviewPackages } from "storybook/internal/preview/globals";

- const NODE_TARGET: Options["target"] = "node20";
+ const NODE_TARGET = "node20.19"; // Minimum Node version supported by Storybook 10

- type BundlerConfig = {
-   bundler?: {
-     exportEntries?: string[];
-     nodeEntries?: string[];
-     managerEntries?: string[];
-     previewEntries?: string[];
-   };
- };

export default defineConfig(async (options) => {
  // reading the three types of entries from package.json, which has the following structure:
  // {
  //  ...
  //   "bundler": {
-  //     "exportEntries": ["./src/index.ts"],
  //     "managerEntries": ["./src/manager.ts"],
-  //     "previewEntries": ["./src/preview.ts"],
+  //     "previewEntries": ["./src/preview.ts", "./src/index.ts"],
  //     "nodeEntries": ["./src/preset.ts"]
  //   }
  // }
-   const packageJson = (await readFile("./package.json", "utf8").then(
-     JSON.parse,
-   )) as BundlerConfig;
+   const packageJson = (
+     await import("./package.json", { with: { type: "json" } })
+   ).default;
+
  const {
    bundler: {
-      exportEntries = [],
      managerEntries = [],
      previewEntries = [],
      nodeEntries = [],
-    } = {},
+    },
  } = packageJson;

  const commonConfig: Options = {
-    splitting: false,
+    splitting: true,
+    format: ["esm"],
-    minify: !options.watch,
    treeshake: true,
-    sourcemap: true,
    // keep this line commented until https://github.com/egoist/tsup/issues/1270 is resolved
    // clean: options.watch ? false : true,
    clean: false,
+    // The following packages are provided by Storybook and should always be externalized
+    // Meaning they shouldn't be bundled with the addon, and they shouldn't be regular dependencies either
+    external: ["react", "react-dom", "@storybook/icons"],
  };

  const configs: Options[] = [];
-
-   // export entries are entries meant to be manually imported by the user
-   // they are not meant to be loaded by the manager or preview
-   // they'll be usable in both node and browser environments, depending on which features and modules they depend on
-   if (exportEntries.length) {
-     configs.push({
-       ...commonConfig,
-       entry: exportEntries,
-       dts: {
-         resolve: true,
-       },
-       format: ["esm", "cjs"],
-       platform: "neutral",
-       target: NODE_TARGET,
-       external: [...globalManagerPackages, ...globalPreviewPackages],
-     });
-   }

  // manager entries are entries meant to be loaded into the manager UI
  // they'll have manager-specific packages externalized and they won't be usable in node
  // they won't have types generated for them as they're usually loaded automatically by Storybook
  if (managerEntries.length) {
    configs.push({
      ...commonConfig,
      entry: managerEntries,
-      format: ["esm"],
      platform: "browser",
-      target: BROWSER_TARGETS,
+      target: "esnext", // we can use esnext for manager entries since Storybook will bundle the addon's manager entries again anyway
-      external: globalManagerPackages,
    });
  }

  // preview entries are entries meant to be loaded into the preview iframe
  // they'll have preview-specific packages externalized and they won't be usable in node
  // they'll have types generated for them so they can be imported when setting up Portable Stories
  if (previewEntries.length) {
    configs.push({
      ...commonConfig,
      entry: previewEntries,
-      dts: {
-        resolve: true,
-      },
-      format: ["esm", "cjs"],
      platform: "browser",
-      target: BROWSER_TARGETS,
+      target: "esnext", // we can use esnext for preview entries since Storybook will bundle the addon's preview entries again anyway
-      external: globalPreviewPackages,
+      dts: true,
    });
  }

  // node entries are entries meant to be used in node-only
  // this is useful for presets, which are loaded by Storybook when setting up configurations
  // they won't have types generated for them as they're usually loaded automatically by Storybook
  if (nodeEntries.length) {
    configs.push({
      ...commonConfig,
      entry: nodeEntries,
-      format: ["cjs"],
      platform: "node",
      target: NODE_TARGET,
    });
```

Next, update the `exports` field in your `package.json` to remove mentions of CJS files.

```diff title="package.json"
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
-      "import": "./dist/index.js",
-      "require": "./dist/index.cjs"
+      "default": "./dist/index.js"
    },
    "./preview": {
-      "types": "./dist/index.d.ts",
-      "import": "./dist/preview.js",
-      "require": "./dist/preview.cjs"
+      "types": "./dist/preview.d.ts",
+      "default": "./dist/preview.js"
    },
-    "./preset": "./dist/preset.cjs",
+    "./preset": "./dist/preset.js",
    "./manager": "./dist/manager.js",
    "./package.json": "./package.json"
  },
```

Update `tsconfig.json`.

```diff title="tsconfig.json"
{
  "compilerOptions": {
    // …
+    "moduleResolution": "bundler",
    // …
-    "module": "commonjs",
+    "module": "preserve",
    // …
-    "target": "ES2020",
+    "target": "esnext",
    // …
-    "lib": ["es2020", "dom", "dom.iterable"],
+    "lib": ["esnext", "dom", "dom.iterable"],
    // …
-    "rootDir": "./src",
+    "rootDir": ".",
  },
-  "include": ["src/**/*"]
+  "include": ["src/**/*", "tsup.config.ts"]
 }
```

Finally, change the `preset.js` file at the top-level of your addon to be ESM. This file used to be CJS because the Storybook Node app only supported CJS.

```diff title="preset.js"
-// this file is slightly misleading. It needs to be CJS, and thus in this "type": "module" package it should be named preset.cjs
-// but Storybook won't pick that filename up so we have to name it preset.js instead
-
-module.exports = require('./dist/preset.cjs');
+export * from './dist/preset.js';
```

### Local addon loading

Because addons are now ESM-only, you must change how you load your own addon in your development Storybook instance. Imports and exports must now follow ESM syntax, and relative paths must use `import.meta.resolve`.


Remove `.storybook/local-preset.cjs` and create `.storybook/local-preset.ts` with the following content:

```ts title=".storybook/local-preset.ts"
import { fileURLToPath } from "node:url";

export function previewAnnotations(entry = []) {
  return [...entry, fileURLToPath(import.meta.resolve("../dist/preview.js"))];
}

export function managerEntries(entry = []) {
  return [...entry, fileURLToPath(import.meta.resolve("../dist/manager.js"))];
}

export * from "../dist/preset.js";
```

Next, update your `main.ts` to reference the new preset file:

```diff title=".storybook/main.ts"
- addons: ["@storybook/addon-docs", "./local-preset.cjs"],
+ addons: ["@storybook/addon-docs", import.meta.resolve("./local-preset.ts")],
```

## Optional changes

The following changes are not strictly required yet, but we recommend making them to improve your users' experience.

### CSF Factories support
To support CSF Factories annotations in your addon, you will need to update your `src/index.ts` file to use the new `definePreviewAddon`. This feature will be part of [CSF Next](../api/csf/csf-next.mdx). This change is highly recommended, as it will help your own users reap the benefits of CSF Factories.

With CSF Factories, users can chain their preview configuration and benefit from better typing and more flexibility. Addons must export annotations to be compatible with this new syntax. CSF Factories will be the default way to write stories in Storybook 11.

```diff title="src/index.ts"
- // make it work with --isolatedModules
- export default {};
+ import { definePreviewAddon } from "storybook/internal/csf";
+ import addonAnnotations from "./preview";
+
+ export default () => definePreviewAddon(addonAnnotations);
```

### Removal of exportEntries

The `exportEntries` property in `package.json`'s `bundler` was used to produce the `index.js` build output from `src/index.ts`. It was compatible with Node.js, rather than strictly with browsers. This build configuration could cause subtle bugs when addon authors exported code in `index.js` for use in the Storybook preview or manager.

In the Storybook 10 [addon-kit](https://github.com/storybookjs/addon-kit), we removed `exportEntries` from the `bundler` config, and we moved `src/index.ts` to be part of `previewEntries` instead. This way, any code exported from `src/index.ts` is bundled for browsers and usable with CSF Next. If you need to export additional code to run in the preview (such as optional decorators), you can add them to `src/index.ts`.

If you need to export code for the manager (such as a `renderLabel` function for the sidebar), you can create a new `src/manager-helpers.ts` file and add it to `managerEntries`, like so:

```diff title="package.json"
  "exports": {
    ".": {
      "types": "./dist/index.d.ts",
      "default": "./dist/index.js"
    },
    "./preview": {
      "types": "./dist/preview.d.ts",
      "default": "./dist/preview.js"
    },
    "./preset": "./dist/preset.js",
    "./manager": "./dist/manager.js",
+    "./manager-helpers": "./dist/manager-helpers.js",
    "./package.json": "./package.json"
  },
  "bundler": {
    "managerEntries": [
+      "src/manager-helpers.ts",
      "src/manager.tsx"
    ]
  }
```

### Build file cleanup

We recommend removing your old build files as you build. This will avoid your `dist` folder growing with expired JS chunks. You may add the following to your `package.json` scripts:

```diff title="package.json"
{
  "scripts": {
+    "prebuild": "node -e \"fs.rmSync('./dist', { recursive: true, force: true })\"",
  }
}
```

### Package keyword changes

We've updated default keywords for addons in the Storybook `addon-kit` template.

```diff title="package.json"
  "keywords": [
-    "storybook-addons",
+    "storybook-addon",
  ],
```

## 10.0.0 full migration guide

For a full list of changes, please visit the [Migration.md](https://github.com/storybookjs/storybook/blob/next/MIGRATION.md#from-version-9x-to-1000) file

## Migration example

For a complete example of an addon updated to support Storybook 10.0, refer to the [Addon Kit migration PR](https://github.com/storybookjs/addon-kit/pull/82).
Once merged, it will demonstrate all the necessary and recommended changes for Storybook 10.

## Releasing

To support Storybook 10.0, we encourage you to release a new major version of your addon. For experimental features or testing, use the `next` tag. This allows you to gather feedback before releasing a stable version.

## Support

If you're having issues with your addon after following this guide, please open a [new discussion](https://github.com/storybookjs/storybook/discussions/new?category=migrations) in our GitHub repository or come talk to us in our [dedicated addon developer channel, `#addons`](https://discord.gg/KKXFQy9sFc) on Discord.
